package cn.kinoko.service.common.export.component;

import cn.kinoko.common.constant.CacheKeyConstant;
import cn.kinoko.config.ExportConfig;
import cn.kinoko.service.common.export.ExportTaskStatusEnum;
import com.github.benmanes.caffeine.cache.Cache;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.unit.DataSize;

import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @author kk
 */
@Slf4j
@Component
public class ExportFileArea {

    @Resource
    private ExportConfig config;
    @Resource
    private Cache<String, Cache<String, Object>> lockCache;

    private final Map<String, File> fileArea = new LinkedHashMap<>();
    private volatile long memoryUsed = 0;

    public byte[] get(String key) {
        File file = fileArea.get(key);
        if (file == null) return null;
        return file.getFileBytes();
    }

    public synchronized boolean put(ExportTask task, byte[] value) {
        if (!evaluateMemoryUsed(value.length)) {
            return false;
        }
        fileArea.put(task.getTaskId(), new File(task.getTaskId(), task, value, LocalDateTime.now().plusSeconds(config.getExpireTime())));
        memoryUsed += value.length;
        return true;
    }

    public synchronized void remove(String key) {
        File remove = fileArea.remove(key);
        memoryUsed -= remove.getFileBytes().length;
    }

    /**
     * 清理过期文件
     *
     * @return 清理的key
     */
    public List<String> clearExpired() {
        // 找出过期的文件
        List<String> expiredFiles = fileArea.values().stream().filter(this::isExpired).map(File::getKey).toList();
        fileArea.entrySet().removeIf(entry -> expiredFiles.contains(entry.getKey()));
        return expiredFiles;
    }

    /**
     * 判断文件是否过期
     * @param file 文件
     * @return true表示过期
     */
    private boolean isExpired(File file) {
        // 先确保资源是否上锁，避免并发下载导致文件被删除，然后判断文件是否过期或失效
        return lockCache.getIfPresent(CacheKeyConstant.LOCK_DOWNLOAD + file.getKey()) == null &&
                (file.getExpireTime().isBefore(LocalDateTime.now()) || file.getTask().getStatus().equals(ExportTaskStatusEnum.FAIL));
    }

    /**
     * 预估文件区域是否有足够的剩余空间
     * @param size 新增size，-1表示不预估
     * @return true 表示有足够空间，false表示无足够空间
     */
    public boolean evaluateMemoryUsed(long size) {
        // 如果新增size≠-1则检查是否超过阈值
        if (size != -1 && DataSize.ofBytes(memoryUsed + size).compareTo(config.getMaxFileAreaSize()) > 0) {
            log.error("[导出文件内存区域] 预估内存占用超过阈值，无法添加，预估：{}MB", DataSize.ofMegabytes(memoryUsed + size));
            return false;
        }
        if (size == -1) {
            // 否则判断当前占用是否超过阈值的一半
            return ((double) memoryUsed / config.getMaxFileAreaSize().toBytes()) > 0.5;
        }
        return true;
    }

    @Data
    @AllArgsConstructor
    public static class File {
        private String key;
        private ExportTask task;
        private byte[] fileBytes;
        private LocalDateTime expireTime;
    }

}
